Skip to content

[WIP] feat: native subagent & workflow observability - #5219

Open
t3dotgg wants to merge 26 commits into
mainfrom
t3code/native-subagent-observability
Open

[WIP] feat: native subagent & workflow observability#5219
t3dotgg wants to merge 26 commits into
mainfrom
t3code/native-subagent-observability

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Aug 2, 2026

Copy link
Copy Markdown
Member

Problem

When a thread spawns subagents, runs a workflow, or drives Codex collab agents, the UI showed nothing useful: subagent tool calls and narration interleaved anonymously into the parent chat, progress ticks spammed the work log, background shells masqueraded as agents, the sidebar showed no status once the turn settled, and Stop only killed the parent turn while the fleet kept burning tokens.

Spec (final, decisions locked): https://n0hbggyouhn1.postplan.dev

image

Solution

Surface agents using only native provider emissions on the current orchestrator — no dependency on orchestrator v2 (#2829), zero migrations, zero new tables. Widened task.* activity payloads ride the existing event-sourced activity path; a client-side fold in client-runtime derives orchestration-v2-shaped subagent state (field names match #4779, so the v2 merge is a mapper swap).

Server

  • Contracts: additive TaskAgentLinkage on all task payloads (role, model, workflow name/phases, run handles, output file, owning agentId), new task.updated event, typed usage, tool attribution (agentId/parentToolUseId)
  • ClaudeAdapter: carries previously-dropped fields, handles task_updated, resolves parent_tool_use_id on tool events, keeps subagent narration out of the parent transcript (leak + Working-timer-reset fix), attributes subagent-internal shells to their owning agent, defensive workflow_progress parse, Workflow run handles
  • Codex: registers multi-agent-v2 children from thread/started/subAgentActivity (root-thread guard from live probe), intercepts child notifications, synthesizes the same task.* lifecycle (idle = resumable, cumulative usage, real names from agentPath)
  • Stop-everything: interrupt stops every live Claude task (Query.stopTask) and interrupts every live Codex child turn before the parent turn
  • Sidebar liveness: ThreadBackgroundLivenessService (in-memory, no persistence) exposes backgroundLiveness: working | monitoring on the thread shell — fleets read Working, watch loops read Monitoring
  • orchestration.getWorkflowScript RPC with TOCTOU-safe containment (open-then-verify inode, realpath under ~/.claude/projects, .js-only, size cap)

Web

  • Agents right-panel surface (the only roster): live workflows as bordered sections with a phase rail (per-phase segments with member status dots) and collapsible phase sections (live open, done collapsed); flat agent rows (no unfold); settled runs collapse to one line under "Earlier"; {} script opens a read-only script view
  • Spawn CTA row in chat: one anchored row per workflow run / direct-spawn batch, exempt from turn folds and overflow, membership pinned at first row (parallel-batch fix), coordinator status authoritative for workflows
  • Quiet timeline: agent-attributed tool rows re-homed to the panel, timelineBypass rows fold into the CTA, background shells stay ordinary work-log rows
  • All in-flight states present as Working (monitoring-pill rule); idle presents as settled

Mobile: same quiet-timeline fold; task.completed kept as terminal signal.

Verification

  • client-runtime: 30 fold tests (identity/activations, usage merges, bounds, cascade, nested agents); web: 1772 incl. quiet-timeline fixtures built from real persisted threads; server: adapter/ingestion/liveness/script-containment suites incl. a symlink-escape test and a stop-everything test
  • Live-tested over multiple rounds: Claude direct spawns + workflows (incl. 10-parallel), Codex collab fleets (10 agents), rerun workflows, monitoring threads — each round's findings fixed against exported wire/DB data

Remaining gaps

  • Codex fixtures should be generated from the captured wire logs (three schema-vs-wire divergences were found live)
  • Settled Codex rows show "assistant message" filler instead of a result summary
  • Script viewer is plain <pre> (no Shiki)
  • Mobile has only the timeline fold, no Agents sheet yet
  • Deferred by design: approval deep-links, transcript drill-in, forwardSubagentText/agentProgressSummaries off

🤖 Generated with Claude Code (Claude Fable 5)

Note

Add native subagent and workflow observability with background liveness tracking and Agents panel

  • Extends the Claude and Codex adapters to emit structured task.started, task.progress, task.updated, and task.completed events carrying agent linkage (model, effort, role, toolUseId, runHandles) and typed usage metrics for subagents and workflow members.
  • Adds a ThreadBackgroundLivenessService that tracks per-thread live task state and exposes a 'working' | 'monitoring' | null status consumed by sidebar status pills and snapshot queries via OrchestrationThreadShell.backgroundLiveness.
  • Introduces an Agents panel (AgentsPanel.tsx) in the web app showing fleet status, live elapsed timers, token counts, and activity text; a CTA row collapses concurrent agent spawns into a single grouped entry in the chat timeline.
  • Adds a getWorkflowScript WebSocket RPC with path containment and size-capped reads, gated behind AuthOrchestrationReadScope.
  • Implements stop-everything semantics in both adapters: interruptTurn now stops all live child tasks/turns before interrupting the parent, and Codex's interrupt handler mirrors this for collab child threads.
  • Risk: task.progress activity IDs changed from evt-task-progress to task-progress:<taskId> for upsert semantics; persisted activity rows written before this change will not be updated by new ticks.

Macroscope summarized 5d68958.


Note

High Risk
Large orchestration and provider-adapter surface area plus a new file-read RPC and interrupt semantics that affect live token spend; changes are well-tested but touch security-sensitive path containment and session control.

Overview
Native subagent and workflow observability on the current orchestrator: widened task.* activity payloads (linkage fields, task.updated, stable per-task progress ids, tool attribution) flow through ingestion; Claude and Codex adapters now emit and map collab/workflow traffic without interleaving subagent narration into the parent chat.

Server behavior changes: ThreadBackgroundLiveness derives working / monitoring for thread shells; interruptTurn stops live child tasks (Claude stopTask, Codex child turn interrupts) before the parent; new getWorkflowScript RPC reads contained .js scripts under ~/.claude/projects.

Web: Agents right-panel (AgentsPanel), spawn CTA rows in chat (exempt from turn folds), quiet timeline (agent-internal / bypass rows folded away), composer Stop when background work outlives the turn, sidebar Working / Monitoring pills.

Mobile mirrors the quiet work-log rules (per-taskId collapse, terminal Codex task.updated rows kept).

Reviewed by Cursor Bugbot for commit 5d68958. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Added an Agents panel showing live and completed subagent workflows, phases, status, usage, and activity.
    • Added workflow script inspection with loading, truncation, and error states.
    • Added Working and Monitoring thread statuses with live background-task counts and a Stop action.
    • Added support for multi-agent collaboration, including nested tasks, progress, retries, and interruptions.
  • Bug Fixes

    • Reduced duplicate and internal activity in the work log while preserving meaningful task progress.
    • Improved task status, ownership, and usage reporting across providers.

…imeline)

Surface Claude Code subagents/workflows and Codex collab agents in the UI
using only native provider emissions. Zero migrations, zero new tables:
widened task.* activity payloads ride the existing event-sourced activity
path, and a client-side fold in client-runtime derives v2-shaped subagent
state (field names match #4779 so the orchestration-v2 merge is mechanical).

Server:
- contracts: TaskAgentLinkage on all task payloads, new task.updated event,
  typed RuntimeTaskUsage, tool attribution (agentId/parentToolUseId)
- ClaudeAdapter: carry subagent_type/workflow_name/tool_use_id/outputFile,
  handle task_updated (was dropped), attribute subagent tool events via
  parent_tool_use_id, defensive workflow_progress parse, Workflow run handles
- CodexSessionRuntime/Adapter: register multi-agent-v2 children from
  thread/started + subAgentActivity, intercept child notifications, and
  synthesize task.* lifecycle (idle=resumable, cumulative usage) [WIP:
  routing is probe-gated per spec]
- ingestion: task.updated + agent-owned tool.progress persisted; wire-slim
  regression test proving agent fields survive to the client

Web:
- Agents right-panel surface (workflow phase groups, direct spawns, static
  status dots, DOM-write elapsed timers, expandable activity ring)
- composer live strip + inline workflow run card (8-row urgency cap)
- quiet timeline: one lifecycle row per agent (collapse by taskId), agent-
  attributed tool rows re-homed to the panel, timelineBypass rows suppressed

Mobile: same quiet-timeline fold; task.completed kept as terminal signal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: native subagent and workflow observability.
Description check ✅ Passed The description explains the problem, solution, UI changes, verification, and remaining gaps, with a screenshot included.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Aug 2, 2026
Comment thread apps/web/src/components/AgentsPanel.tsx
Comment thread packages/client-runtime/src/state/subagentRuntime.ts Outdated
Comment thread packages/client-runtime/src/state/subagentRuntime.ts
Comment thread apps/web/src/components/chat/WorkflowRunCard.tsx Outdated
Comment thread packages/client-runtime/src/state/subagentRuntime.ts
Comment thread apps/server/src/provider/Layers/CodexAdapter.ts
Comment thread packages/client-runtime/src/state/subagentRuntime.ts
return;
}

if (yield* interceptCollabChildNotification(notification)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/CodexSessionRuntime.ts:1079

Children tracked only via collabReceiverTurns (from a prior collabAgentToolCall) never emit the synthetic collabAgent/* lifecycle events this change introduces, so their task state stays missing or stuck. handleRawNotification returns early when shouldSuppressChildConversationNotification is true — which covers thread/started, turn/started, turn/completed, status changes, token usage, and thread/closed — before interceptCollabChildNotification runs at line 1079. Those children are never registered through thread/started, and even subAgentActivity-registered children never emit collabAgent/turnStarted, collabAgent/turnCompleted, collabAgent/statusChanged, collabAgent/tokenUsage, or collabAgent/closed because the suppression check intercepts those methods first. Consider moving interceptCollabChildNotification ahead of the childParentTurnId suppression check, or making the suppression skip notifications addressed to registered child threads.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/provider/Layers/CodexSessionRuntime.ts around line 1079:

Children tracked only via `collabReceiverTurns` (from a prior `collabAgentToolCall`) never emit the synthetic `collabAgent/*` lifecycle events this change introduces, so their task state stays missing or stuck. `handleRawNotification` returns early when `shouldSuppressChildConversationNotification` is true — which covers `thread/started`, `turn/started`, `turn/completed`, status changes, token usage, and `thread/closed` — before `interceptCollabChildNotification` runs at line 1079. Those children are never registered through `thread/started`, and even `subAgentActivity`-registered children never emit `collabAgent/turnStarted`, `collabAgent/turnCompleted`, `collabAgent/statusChanged`, `collabAgent/tokenUsage`, or `collabAgent/closed` because the suppression check intercepts those methods first. Consider moving `interceptCollabChildNotification` ahead of the `childParentTurnId` suppression check, or making the suppression skip notifications addressed to registered child threads.

Comment thread packages/client-runtime/src/state/subagentRuntime.ts
@macroscopeapp

macroscopeapp Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

6 blocking correctness issues found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

…w card

Per live-test feedback the roster rendered three times at once (panel, card,
strip). New rule: the Agents panel is the only roster. The chat gets one
anchored CTA row per spawn batch (workflow run, or a turn's direct spawns):
'Kicked off N subagents · <workflow> — <phase> · N active · Σ tok — Open
Agents'. Live status derives from the shared panel model at render time;
the row freezes to past tense on settle. Strip and card components deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx Outdated
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx
Live-test round 2 fixes:
- Shells/monitors/plan tasks no longer masquerade as subagents: taskType
  rides on every task payload (adapter linkage + ingestion allowlist) and
  the fold excludes non-agent task types from the roster. 'Run 12s stall'
  background shells stay in the ordinary work log.
- The spawn CTA no longer says completed while a workflow is mid-flight:
  for workflow batches the coordinator's own terminal state is authoritative
  (dynamic spawns can make the known-member list momentarily all-settled).
- Agents panel rows are flat status lines: the per-agent unfold (recent
  tool-call feed) is gone. The only expansion is run-granularity: settled
  workflow runs collapse to one summary line under 'Earlier', click to list
  members. Live workflows and direct spawns sort first in bordered sections
  with settled/total counts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium

return `direct:${entry.turnId ?? "no-turn"}`;

In collapseDerivedWorkLogEntries, direct-spawn tasks whose turnId is absent all share the same direct:no-turn group key, so agents from different turns (or legacy rows) collapse into a single CTA row with a combined agentTaskIds list — distinct spawn events are silently merged. The fallback in agentSpawnGroupKey should be scoped to a per-batch or per-activity identifier rather than a global "no-turn" string.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/session-logic.ts around line 845:

In `collapseDerivedWorkLogEntries`, direct-spawn tasks whose `turnId` is absent all share the same `direct:no-turn` group key, so agents from different turns (or legacy rows) collapse into a single CTA row with a combined `agentTaskIds` list — distinct spawn events are silently merged. The fallback in `agentSpawnGroupKey` should be scoped to a per-batch or per-activity identifier rather than a global `"no-turn"` string.

Comment thread apps/web/src/session-logic.ts
Comment thread packages/client-runtime/src/state/subagentRuntime.ts
const taskId = asString(payload.taskId);
if (!taskId) break;
if (isBackgroundTaskActivity(payload)) break;
const agent = getOrCreate(agents, taskId, payload, at);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium state/subagentRuntime.ts:521

When task.updated is the first event available for an agent (its start row has aged out of retention), getOrCreate creates it with activationCount: 0, and the task.updated branch never initializes the count to 1 — unlike the task.progress and task.completed branches. The agent appears in the roster with zero activations even while running or terminal, and a subsequent task.started event would incorrectly be treated as the first activation, setting activationCount back to 1. Consider initializing activationCount to 1 in the task.updated branch when the agent is newly created.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/client-runtime/src/state/subagentRuntime.ts around line 521:

When `task.updated` is the first event available for an agent (its start row has aged out of retention), `getOrCreate` creates it with `activationCount: 0`, and the `task.updated` branch never initializes the count to 1 — unlike the `task.progress` and `task.completed` branches. The agent appears in the roster with zero activations even while running or terminal, and a subsequent `task.started` event would incorrectly be treated as the first activation, setting `activationCount` back to 1. Consider initializing `activationCount` to 1 in the `task.updated` branch when the agent is newly created.

// guarantee).
const spawnRowIndex = new Map<string, number>();
for (const entry of entries) {
const isTaskRow =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/session-logic.ts:859

collapseDerivedWorkLogEntries converts any task.progress/task.completed row that has a taskId into an agent-spawn CTA, unless isBackgroundTask is true. When taskType is absent (e.g. older persisted rows), isBackgroundTaskActivity returns false, so ordinary shell/monitor/plan task rows get misclassified as subagent spawns and grouped with real agent rows in the timeline instead of remaining normal work-log entries. Consider gating the agent-spawn CTA path on a positive signal (e.g. isWorkflowCoordinator or a known subagent taskType) rather than treating "not background" as "is a subagent."

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/session-logic.ts around line 859:

`collapseDerivedWorkLogEntries` converts any `task.progress`/`task.completed` row that has a `taskId` into an agent-spawn CTA, unless `isBackgroundTask` is true. When `taskType` is absent (e.g. older persisted rows), `isBackgroundTaskActivity` returns false, so ordinary shell/monitor/plan task rows get misclassified as subagent spawns and grouped with real agent rows in the timeline instead of remaining normal work-log entries. Consider gating the agent-spawn CTA path on a positive signal (e.g. `isWorkflowCoordinator` or a known subagent `taskType`) rather than treating "not background" as "is a subagent."

Comment thread packages/client-runtime/src/state/subagentRuntime.ts
Rerun workflows looked invisible: the launching turn settles in seconds
('Worked for 8.9s') and turn-folding collapsed all its work entries —
including the spawn CTA — while the fleet runs on in the background. CTA
rows are now exempt from turn folds and pinned outside the '+N tool calls'
overflow toggle, so a live run is always visible at its spawn point. Each
rerun gets its own CTA row (grouping keys on the coordinator id, which is
unique per run).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(entry) => entry.agentSpawn !== undefined,
);
const hiddenEntries = overflowCandidates.slice(0, -MAX_VISIBLE_WORK_LOG_ENTRIES);
const visibleEntries = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium chat/MessagesTimeline.logic.ts:508

The visibleGroupedEntries are split into pinnedSpawnEntries and overflowCandidates and then concatenated with spawn entries first, so a group like [normalWork, agentSpawn] renders as [agentSpawn, normalWork]. This reorders the timeline whenever an agent-spawn CTA is interleaved with ordinary work rows. Consider filtering hidden entries from the original order instead of repartitioning and concatenating.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/chat/MessagesTimeline.logic.ts around line 508:

The `visibleGroupedEntries` are split into `pinnedSpawnEntries` and `overflowCandidates` and then concatenated with spawn entries first, so a group like `[normalWork, agentSpawn]` renders as `[agentSpawn, normalWork]`. This reorders the timeline whenever an agent-spawn CTA is interleaved with ordinary work rows. Consider filtering hidden entries from the original order instead of repartitioning and concatenating.

Comment thread apps/web/src/components/chat/MessagesTimeline.logic.ts
t3dotgg and others added 3 commits August 2, 2026 02:04
Live-test finding: statuses drifted (waiting/stalled agents alarming or
reading wrong) while fleets ran. Adopts the monitoring-pill rule from the
PR-monitoring design: one steady in-flight presentation.

- Panel and CTA: pending/running/waiting all render as Working (sky, no
  amber); detail stays in the activity sub-line; footer shows one working
  count. Only settled states differentiate (completed/failed/stopped).
- Fold: when a workflow coordinator settles, members that never received
  their own terminal row cascade to the coordinator's outcome (completed,
  or interrupted on failure) instead of reading as working forever.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live-test: the CTA only appeared after the workflow finished. Two causes,
reproduced against the exact persisted thread data:
- workEntryIndicatesToolNeutralStatus swallowed spawn rows mid-run (they
  derive from task.progress, tone 'thinking' = neutral) — visible only
  once a terminal row flipped them to success. CTA rows are now exempt.
- collapse-by-group let the newest progress tick's id/createdAt/turnId win,
  drifting the row to the bottom of the timeline mid-run. The group anchor
  (spawn point) is now pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live-test: a thread running a 9-agent workflow and a thread babysitting a
PR both showed no sidebar status once their turns settled. The pill only
reflected turn/session state.

- Server: in-memory per-thread background liveness registry fed by the
  task lifecycle ingestion already processes (no persistence, no
  migration); exposed additively on the thread shell as
  backgroundLiveness: working | monitoring | null.
- Vocabulary (per Theo): two states only. Agent/workflow fleets present as
  plain Working; Monitoring is reserved for watch loops (monitor tasks and
  turn-outliving background shells — PR babysitting, log tails) when they
  are the only live work.
- Web: pill resolver and SidebarV2 status honor backgroundLiveness with
  the same recede treatment as Working (inbox-zero); Monitoring gets a
  steady label, no shimmer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effect service conventions review: one clear violation (new shared mutable state held in a module global and consumed by two Effect services) plus one file-layout nit. Everything else in the changed Effect code (namespace imports from effect/* subpaths, per-session Map state inside Effect.gen, contract schemas) looks conformant.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts Outdated
Comment thread apps/server/src/orchestration/threadBackgroundLiveness.ts Outdated
Comment thread apps/server/src/orchestration/threadBackgroundLiveness.ts Outdated
Comment thread apps/server/src/orchestration/threadBackgroundLiveness.ts Outdated
Comment thread apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts Outdated
Live-test: plain Task-tool subagents vanished from the Agents panel and
CTA. The SDK reports them as taskType 'local_agent', which the agent-type
allowlist didn't anticipate — real agents were silently classified as
background work. Classification is now a denylist (shell/local_bash/
monitor/plan are background; anything else, including future agent-flavored
type names, is an agent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/client-runtime/src/state/subagentRuntime.ts Outdated
First live Codex probe (5 collab agents on gpt-5.6-sol) surfaced three
gaps, fixed against the persisted wire data:
- Children registered only via subAgentActivity (no thread/started with a
  spawn source), so no task.started ever formed and rows carried bare
  thread-id titles. subAgentActivity 'started' now synthesizes the
  task.started with agentPath-leaf naming.
- Codex child rows are ALL timelineBypass, so the quiet-timeline filter
  suppressed every row before the CTA could form — a Codex fleet had no
  chat presence at all. Bypassed agent lifecycle rows now feed the CTA
  collapse (still max one row per batch); non-agent bypassed rows stay
  suppressed.
- Idle now counts as not-live in the sidebar registry: an all-idle
  (resting, resumable) fleet no longer pins the thread at Working.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/session-logic.ts
t3dotgg and others added 4 commits August 2, 2026 02:53
Live-test round 2 on Codex direct spawns:
- Idle (resting, resumable) agents wore a sky dot and sat in the live
  section, reading as stuck in-progress after the work finished. Idle now
  renders muted and sorts with settled.
- Progress rows carried the bare child thread id as title, clobbering the
  real name (math_one → UUID) from task.started. The runtime now stamps
  the registered child's identity (nickname/role/agentPath) on every
  synthetic collabAgent event, the adapter only emits title when it has a
  real name, and subAgentActivity registration derives a nickname from the
  agentPath leaf.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live probe on 10 parallel collab agents: the wire emits subAgentActivity
{agentPath: '/root', kind: interacted} about the ROOT thread during collab
runs. Registration adopted the root as a child, so every subsequent root
notification — including the final assistant message and turn/completed —
was intercepted into the agent panel instead of the chat. The parent
looked hung ('Working 5m') after all subagents finished, with its report
riding an 'assistant message' row on the root's panel entry.

Registration now refuses the session's root thread (by id and by /root
path), and interception has a belt-and-braces root check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… row

Claude background subagents settle between turns, so their completion rows
arrive under later synthetic turn ids (or none). Batch keying by each
row's own turn splintered ten parallel spawns into a stream of 'Kicked
off N subagents' rows (live thread 7ac7ef05: completions spread across 8
different turn ids). Membership is now decided once at the first row seen
per taskId — task.started rows (which carry the true spawn turn) seed the
batch and collapse into its CTA instead of being skipped.

Repro test derives exactly one CTA with all 10 agents from the persisted
thread export.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nscript

Two live-test findings with parallel Claude subagents, one root cause:
even with forwardSubagentText off, the SDK forwards subagent-owned
messages tagged parent_tool_use_id, and the adapter emitted them as
parent conversation.

- Subagent assistant snapshots became interleaved leak messages ('sleep
  ran successfully…') AND spawned a synthetic turn per completion — which
  is also why the Working timer kept resetting to 0: each synthetic turn
  restarted the elapsed clock. Subagent-owned assistant messages now only
  advance the resume cursor.
- Subagent-owned text/thinking stream blocks wrote into the parent
  transcript; they are now dropped (tool_use blocks still flow, with
  agentId attribution, for the quiet-timeline re-homing).

Subagent results still reach the UI through the task.* lifecycle
(task_notification summaries) — the panel loses nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/server/src/orchestration/workflowScriptQuery.ts Outdated
Comment thread apps/server/src/orchestration/workflowScriptQuery.ts
Stop button now kills the fleet, not just the parent turn — the moment
users need it most is a runaway spawn:
- Claude: interruptTurn stops every non-terminal tracked task via the
  SDK's Query.stopTask (best-effort per task, capped concurrency) before
  interrupting the turn. Live-task set maintained from the task lifecycle.
- Codex: children are threads with their own turns; interruptTurn now
  turn/interrupts each live child turn (tracked from intercepted child
  turn/started/completed) before the parent turn.

Agents panel phases are collapsible per the Claude Code background-tasks
pattern: live phases open by default, done phases collapse to header +
member dot row; user toggles stick.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/server/src/provider/Layers/ClaudeAdapter.ts
CI:
- lint: namespace node:fs/promises import; workflowScriptQuery tests use
  @effect/vitest it.effect instead of Effect.runPromise
- ProjectionSnapshotQuery test expects the new backgroundLiveness field

Review findings (verified against source, all real):
- Critical: TOCTOU in workflow script read — open first, then verify the
  opened inode matches a fresh lstat of the resolved path; short reads
  honored via bytesRead (trailing-NUL fix folded in)
- High: stopTask invoked through context.query so SDK 'this' binding
  survives; previously every stop silently failed via Effect.ignore
- Medium: subagent stream filter now drops only text/thinking deltas, so
  attributed tool items keep their input_json_delta frames
- Medium: liveness terminal rows clear BOTH buckets (terminal ticks often
  omit taskType, stranding monitor entries in the agents bucket)
- Medium: nested agents (agentId + agent taskType) stay in the roster;
  only agent-owned shells are background. monitor_mcp/dream added to the
  non-agent denylist
- Conventions: liveness registry is now ThreadBackgroundLivenessService
  (Context.Service + Layer, per-instance state, memoized shared Live
  provided at both consumers); OrchestrationGetWorkflowScriptError carries
  cause like sibling RPC errors, and readWorkflowScript forwards causes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/server/src/orchestration/ThreadBackgroundLiveness.ts

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effect service conventions: two findings on the new ThreadBackgroundLiveness service. The rest of the changed Effect code (namespace imports, Effect.fn usage, yield* ThreadBackgroundLivenessService acquisition inside make) looks consistent with the conventions.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/orchestration/Services/ThreadBackgroundLiveness.ts Outdated
Comment thread apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts Outdated
t3dotgg and others added 2 commits August 2, 2026 06:17
- workflowScriptQuery: nodeBuiltinImport suppression header (raw fs access
  is deliberate: containment needs realpath/inode identity, and the file
  lives outside any workspace root the FileSystem service is scoped to)
- ClaudeAdapter: DateTime.makeUnsafe instead of new Date() for the
  task_updated end_time conversion
- stop-everything test: wait on the task.* event stream instead of a
  wall-clock setTimeout (which also hung under the test clock)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…layer requirement

Consolidates the split Services/+Layers/ files into one canonical module
(tag + make + layer), counts nested agents toward liveness, and removes
the per-consumer Layer.provide self-provides so ThreadBackgroundLivenessService
shows up as a requirement in layer types. A single shared instance is
provideMerged at OrchestrationInfrastructureLayerLive and at each test
composition root.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/server/src/orchestration/ThreadBackgroundLiveness.ts

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two Effect service convention issues in the new modules; details inline.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/orchestration/workflowScriptQuery.ts Outdated
Comment thread apps/server/src/orchestration/ThreadBackgroundLiveness.ts Outdated
…ion fix

- ThreadBackgroundLiveness now exports canonical make/layer consumed via
  namespace import, and references the inferred interface as
  ThreadBackgroundLivenessService["Service"].
- recordTaskLiveness drops any prior entry for a taskId on every path, so
  a task reclassified across transitions (untyped -> shell, or newly
  inert/agent-owned) moves buckets instead of leaving a stale duplicate
  pinning the thread's status. Regression test added.
- workflowScriptQuery constructs OrchestrationGetWorkflowScriptError
  inline at each failure boundary instead of via a fail() wrapper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One finding on the new orchestration RPC error's shape. The service module hoist (orchestration/ThreadBackgroundLiveness.ts with inline interface, make, layer, Service["Service"], requirement kept on the consuming layers) and the workflowScriptQuery error/cause cleanup from earlier rounds all look right.

Posted via Macroscope — Effect Service Conventions

Comment thread packages/contracts/src/orchestration.ts Outdated
…iptError

Replaces the unstructured message field with a reason discriminator plus
the offending scriptPath; message is derived from reason so wire shape
stays serializable and failure kind is matchable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Latest build

image

When a turn settles but native background work stays live (subagent
fleets, workflows, monitor loops), the composer stop button is gone and
nothing on the thread offers a way to stop it. Adds a composer banner --
'N agents working in the background' / 'Monitoring in the background' --
driven by the shell's backgroundLiveness, with a Stop button wired to
the existing stop-everything interrupt (session-scoped, kills all live
tasks, no active turn required). Stopping state holds until liveness
clears.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/components/ChatView.tsx Outdated
t3dotgg and others added 2 commits August 3, 2026 14:54
…anel

TaskAgentLinkage gains an optional effort field alongside model. The
Claude adapter seeds both at task_started (Agent tool input overrides,
falling back to the session's model/effort selection) and refines model
from the subagent's own assistant snapshots (authoritative API id);
both repeat on every task.* row via the linkage bundle, so they persist
in activity payloads and survive retention. The fold carries them into
RuntimeSubagent and the Agents panel renders a compact
'sonnet-5[1m] · high' chip in the stats line.

Codex children don't expose per-child model on the wire yet; the field
stays absent there until the app-server surfaces it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ching, mobile terminal signal

- Legacy task rows (no taskType, no linkage markers) keep pre-upgrade
  work-log behavior instead of reclassifying as subagents; membership is
  sticky per taskId so marker-less terminal rows still reach their agent.
- Session death derives interruption: foldSubagentActivities takes
  sessionLive and marks orphaned live agents interrupted (idle/settled
  preserved), matching the server registry clearing on session.exited.
- Codex children stamp their parent's spawn turn on every synthetic
  collabAgent/* event, and bypassed task.started rows may seed CTAs, so
  separate fleets anchor at their own spawn points instead of merging
  into one 'direct:no-turn' batch.
- A late task.started after a terminal task.updated no longer reopens
  the run (guard on status, not activationCount).
- Workflow retries count once per attempt (bump only in applyStatus).
- Duplicate completions are fully idempotent (first result wins);
  provider endedAt wins over ingestion time on the settling transition.
- CTA and settled-workflow token totals follow the panel-footer rule:
  coordinator usage only counts when no member rows exist.
- Members with unknown phase indices render under unphasedMembers
  instead of vanishing; idle members keep their phase active.
- Mobile keeps terminal bypassed task.updated rows (idle/failed/
  interrupted) — the only terminal signal Codex children emit.
- Stop-banner 'Stopping...' state resets on thread switch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/client-runtime/src/state/subagentRuntime.ts Outdated
Comment thread apps/mobile/src/lib/threadActivity.ts
…ollapse

- A task.completed arriving after a terminal task.updated (common Claude
  ordering) now enriches the settled agent — fills missing result/error
  and max-merges final usage — while status and timestamps stay frozen
  (duplicates still cannot replace the first result).
- Codex status patches repeat the child identity bundle (role, known
  name, agentPath) so rows are self-describing when the start row ages
  out of retention, and mobile terminal labels get real names.
- Mobile task-row identity extraction and collapse include task.updated,
  so reused Codex children fold into one row per child instead of
  stacking anonymous 'Task idle' rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
}
return [];
}
case "collabAgent/tokenUsage": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/CodexAdapter.ts:669

The collabAgent/tokenUsage mapping emits task.progress using base from runtimeEventBase(event, ...), which only populates turnId when event.turnId is present. The synthetic token-usage event is the one collab event emitted without the child's spawnTurnId, so the emitted task.progress event carries turnId: null. When ingestion collapses by taskId, a null turnId overwrites the merged row's existing turnId; if token usage is the latest patch, the agent row loses its parent-turn linkage and is grouped in the wrong timeline location. Preserve the child's turnId (e.g., from payload) and include it in base before emitting this event.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around line 669:

The `collabAgent/tokenUsage` mapping emits `task.progress` using `base` from `runtimeEventBase(event, ...)`, which only populates `turnId` when `event.turnId` is present. The synthetic token-usage event is the one collab event emitted without the child's `spawnTurnId`, so the emitted `task.progress` event carries `turnId: null`. When ingestion collapses by `taskId`, a null `turnId` overwrites the merged row's existing `turnId`; if token usage is the latest patch, the agent row loses its parent-turn linkage and is grouped in the wrong timeline location. Preserve the child's `turnId` (e.g., from `payload`) and include it in `base` before emitting this event.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (10)
apps/server/src/orchestration/workflowScriptQuery.ts (1)

25-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Record the provider decision for the hardcoded Claude scripts root.

scriptsRoot() returns ~/.claude/projects. Workflow scripts produced by other adapters resolve outside that root and fail with outside-root. This PR also adds Codex workflow observability, so the Agents "{} script" affordance will not work for Codex runs.

State the intended behavior for Codex, Cursor, Grok, and OpenCode. If only Claude persists workflow scripts today, document that in the module header so the limitation is explicit.

As per coding guidelines: "Provider-shaped features require a deliberate decision for each provider adapter: Codex, Claude, Cursor, Grok, and OpenCode."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/orchestration/workflowScriptQuery.ts` around lines 25 - 27,
Document the provider decision in the module header for the hardcoded
scriptsRoot() path: explicitly state the intended workflow-script behavior for
Codex, Claude, Cursor, Grok, and OpenCode, including that only Claude currently
persists scripts if that is the supported behavior. Ensure the documentation
makes clear which providers support the Agents “script” affordance and avoids
implying that all adapters use ~/.claude/projects.

Source: Coding guidelines

apps/server/src/orchestration/workflowScriptQuery.test.ts (1)

10-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Do not write test fixtures into the real home directory.

Lines 10-13 create files inside the developer's or CI runner's actual ~/.claude/projects, which is the same directory the Claude harness uses. Line 14 uses a fixed name in os.tmpdir(). Two concurrent runs collide, and afterAll deletes state under the real home directory.

Make the scripts root injectable in workflowScriptQuery.ts (for example an optional root parameter that defaults to scriptsRoot()), then point the test at a mkdtempSync directory. This also removes the need for the afterAll cleanup of real user state.

Also applies to: 23-26

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/orchestration/workflowScriptQuery.test.ts` around lines 10 -
15, The test currently writes fixtures into the real home directory and uses a
colliding fixed temporary filename. Make the scripts root injectable in the
relevant workflow script query API, defaulting to scriptsRoot(), then have the
test create an isolated mkdtempSync directory and place both fixtures under it.
Update calls and cleanup to use only this temporary directory, removing afterAll
cleanup of real user state.
apps/web/src/rightPanelStore.ts (1)

52-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider keeping the storage version at 7.

The change adds one surface kind. migratePersistedRightPanelState already tolerates the previous shape, because it only rewrites file and terminal surfaces and passes other kinds through. Raising the version to 8 discards every user's open right-panel tabs on upgrade without a correctness benefit.

If you keep version 8, that is a deliberate reset. If not, revert to 7.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/rightPanelStore.ts` at line 52, Revert
RIGHT_PANEL_STORAGE_VERSION to 7 because migratePersistedRightPanelState already
preserves the newly added surface kind and no reset is required.
apps/web/src/components/chat/MessagesTimeline.logic.ts (1)

531-533: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Compute onlyToolEntries from the hidden entries.

The toggle describes the hidden rows. visibleGroupedEntries includes the pinned spawn entries, which are never hidden. A spawn entry that is not tool-like flips onlyToolEntries to false even when every hidden row is a tool call, which changes the toggle label.

♻️ Proposed change
-              onlyToolEntries: visibleGroupedEntries.every((entry) =>
-                workLogEntryIsToolLike(entry),
-              ),
+              onlyToolEntries: hiddenEntries.every((entry) => workLogEntryIsToolLike(entry)),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/chat/MessagesTimeline.logic.ts` around lines 531 -
533, The onlyToolEntries calculation currently uses visibleGroupedEntries, which
includes pinned spawn entries; update the logic around onlyToolEntries to
evaluate the hidden entries collection instead, while preserving the existing
workLogEntryIsToolLike predicate and toggle behavior.
apps/web/src/session-logic.ts (2)

652-672: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the quiet-timeline doc block onto deriveWorkLogEntries.

The comment at Lines 652-661 documents the work-log filtering contract of deriveWorkLogEntries. It now sits directly above isAgentTaskStartedActivity, which carries its own doc comment on Line 662. A reader (and any doc tooling) attaches the block to the wrong function.

Move the block to Line 712, immediately above deriveWorkLogEntries.

As per coding guidelines: "Use comments mainly to describe how a function is used; avoid annotating every line of behavior, and move comments when code moves."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/session-logic.ts` around lines 652 - 672, Move the
quiet-timeline guarantee doc block from above isAgentTaskStartedActivity to
immediately above deriveWorkLogEntries, preserving its text unchanged; keep the
separate isAgentTaskStartedActivity documentation directly above that function.

Source: Coding guidelines


882-895: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Carry parentAgentId into the derived entry and key by it.

toDerivedWorkLogEntry stores taskId and agentSpawn.workflowId, but not parentAgentId. If parentAgentId exists without a legacy :wf: encoding, agentSpawnGroupKey falls through to direct:<turnId> and splits the same workflow members into separate chat CTA rows from what the Agents panel groups under one coordinator.

Also update the :wf:-slot check only after copying the parent link so this only affects unencoded workflow members.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/session-logic.ts` around lines 882 - 895, Update
DerivedWorkLogEntry and toDerivedWorkLogEntry to preserve parentAgentId, then
update agentSpawnGroupKey to use the copied parentAgentId for workflow grouping
when no legacy :wf: taskId encoding exists. Keep the existing :wf: parsing
precedence unchanged, and apply the parent-link fallback before the coordinator
and direct turnId cases.
apps/server/src/provider/Layers/ClaudeAdapter.test.ts (1)

1453-1601: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add focused tests for task_updated and workflow member synthesis.

The two new tests cover interrupt fan-out and model/effort inheritance. Two behaviors with more branching remain untested in this file:

  • task_updated: the CLAUDE_TASK_PATCH_STATUS mapping (killedcancelled, pausedidle), removal from liveTaskIds on a terminal patch, and the endedAt conversion.
  • emitWorkflowMemberProgress: the <coordinatorTaskId>:wf:<index> member id, per-index dedupe, and workflowAgentStatus.

Both are backend behavior changes, so the guidelines require focused tests. Do you want me to draft them in the same harness style?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/Layers/ClaudeAdapter.test.ts` around lines 1453 -
1601, Add focused harness-style tests in ClaudeAdapter.test.ts for task_updated
and workflow member synthesis. Cover killed→cancelled and paused→idle status
mapping, terminal patch removal from liveTaskIds, endedAt conversion, and
emitWorkflowMemberProgress behavior including <coordinatorTaskId>:wf:<index>
IDs, per-index deduplication, and workflowAgentStatus.

Source: Coding guidelines

apps/server/src/orchestration/ActivityPayloadProjection.test.ts (1)

5-15: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Set the activity kind per test so the task-lifecycle case is really exercised.

The helper hardcodes kind: "tool.completed". The second test is named for task lifecycle payloads, but it still passes a tool activity. If projectActivityPayload branches on kind, this test never reaches the task branch and the regression tripwire described in the file comment does not fire.

💚 Proposed change
-function activity(payload: Record<string, unknown>): OrchestrationThreadActivity {
+function activity(
+  payload: Record<string, unknown>,
+  kind = "tool.completed",
+): OrchestrationThreadActivity {
   return {
     id: "activity-1",
     tone: "tool",
-    kind: "tool.completed",
+    kind,
     summary: "Tool",
     payload,
     turnId: null,
     createdAt: "2026-08-01T10:00:00.000Z",
   } as unknown as OrchestrationThreadActivity;
 }
-    const source = activity({
-      taskId: "task-9",
+    const source = activity(
+      {
+        taskId: "task-9",
...
-      timelineBypass: true,
-    });
+        timelineBypass: true,
+      },
+      "task.started",
+    );

Also applies to: 47-62

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/orchestration/ActivityPayloadProjection.test.ts` around lines
5 - 15, The activity helper currently hardcodes the tool kind, preventing
task-lifecycle tests from exercising the task branch. Update activity and its
callers so each test supplies the appropriate activity kind, preserving
tool.completed for tool tests and using the task lifecycle kind in the second
test.
apps/server/src/provider/Layers/ClaudeAdapter.ts (1)

4358-4375: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Log stopTask failures instead of discarding them.

Every stopTask rejection is swallowed twice: catch: () => undefined and Effect.ignore. A provider that refuses to stop background tasks then produces no signal at all, and users see agents keep burning tokens after pressing Stop. Keep the best-effort semantics, but record the failure.

♻️ Proposed change to keep failures observable
-        yield* Effect.forEach(
-          liveIds,
-          (taskId) =>
-            Effect.tryPromise({
-              // Invoke through the query object: SDK methods rely on `this`.
-              try: () => context.query.stopTask!(taskId),
-              catch: () => undefined,
-            }).pipe(Effect.ignore),
-          { concurrency: 8, discard: true },
-        );
+        yield* Effect.forEach(
+          liveIds,
+          (taskId) =>
+            Effect.tryPromise({
+              // Invoke through the query object: SDK methods rely on `this`.
+              try: () => context.query.stopTask!(taskId),
+              catch: (cause) => cause,
+            }).pipe(
+              Effect.catchCause((cause) =>
+                Effect.logWarning("claude.task.stop-failed", { threadId, taskId, cause }),
+              ),
+            ),
+          { concurrency: 8, discard: true },
+        );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/Layers/ClaudeAdapter.ts` around lines 4358 - 4375,
Update the stop-task cleanup around context.query.stopTask to preserve
best-effort per-task execution while logging each rejection or thrown failure
with the taskId and error details. Replace the silent catch/ignore handling
without allowing one failed stopTask call to prevent remaining tasks from being
attempted or the subsequent interrupt from running.
apps/server/src/provider/Layers/CodexAdapter.ts (1)

760-766: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Record the intentional omission for Cursor, Grok, and OpenCode child task events.

This PR adds task.* child events for Codex and Claude only, while Cursor, Grok, and OpenCode do not emit collabAgent/...* child events. Capture that decision in the PR description or a code comment so the provider-level behavior is explicit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/Layers/CodexAdapter.ts` around lines 760 - 766,
Document in a nearby comment to mapToRuntimeEvents that Cursor, Grok, and
OpenCode intentionally do not map collabAgent/... child task events because
those providers do not emit them; clarify that task.* child events are currently
supported only for Codex and Claude.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/server/src/orchestration/workflowScriptQuery.test.ts`:
- Around line 16-21: Update the symlink setup and assertions in the workflow
script query test around NodeFS.symlinkSync and readWorkflowScript: only
tolerate an already-existing link, rethrow unexpected or permission errors,
verify the link exists before querying, and assert that the returned Failure
reason is the containment-specific value rather than checking only _tag.

In `@apps/server/src/orchestration/workflowScriptQuery.ts`:
- Around line 29-38: Update readWorkflowScript and getWorkflowScript to carry
the caller’s threadId, then resolve the workflow script location recorded for
that thread in runHandles.scriptPath before accepting the requested path hint.
Require the resolved requested path to match the thread’s recorded script
location, while retaining ~/.claude/projects as a secondary containment check.
Reject mismatches through the existing invalid-path error flow.

In `@apps/server/src/provider/Layers/CodexAdapter.ts`:
- Around line 669-700: Update the token usage parsing in the
collabAgent/tokenUsage case to define and reuse the existing count validator
before validating totalTokens. Require totalTokens to pass count, return [] when
it is undefined, and assign the validated value to RuntimeTaskUsage; reuse local
validated values for the other fields instead of calling count repeatedly.

In `@apps/server/src/provider/Layers/CodexSessionRuntime.ts`:
- Around line 1109-1122: The interceptCollabChildNotification switch currently
drops child error notifications, leaving the child marked running. Add a case
for "error" that emits a collabAgent/statusChanged event with type "systemError"
and the child identity/status payload required by mapCollabAgentEvent to produce
a failed task update; preserve existing handling for closed and other
notifications.

In `@apps/web/src/components/chat/MessagesTimeline.logic.ts`:
- Around line 504-512: Update the work-group entry selection around
pinnedSpawnEntries, hiddenEntries, visibleEntries, and renderedEntries to
preserve the original visibleGroupedEntries chronological order. Select retained
entries by their indices rather than concatenating all spawn rows with the
overflow tail, and ensure the expanded path renders the full ordered group while
the collapsed path renders only the retained ordered entries.

In `@apps/web/src/components/ChatView.tsx`:
- Around line 4209-4224: Update handleStopBackgroundWork so every
interruptThreadTurn failure resets isStoppingBackgroundWork, including when
isAtomCommandInterrupted(result) is true, while preserving the existing
error-setting path only for non-interrupted failures.

In `@apps/web/src/components/Sidebar.logic.ts`:
- Around line 443-450: Reorder the backgroundLiveness branches in both status
resolvers in apps/web/src/components/Sidebar.logic.ts:443-450 and
apps/web/src/components/Sidebar.logic.ts:626-646. In the first resolver, keep
session?.status === "error" ahead of background statuses; in the second, keep
hasPlanReadyPrompt ahead of the background-liveness pill branches so error and
actionable states take precedence.
- Line 135: Update the status priority mapping around Monitoring so Monitoring
ranks below the active Working and Connecting statuses, or adjust
resolveProjectStatusIndicator to break equal-priority ties in favor of active
work. Preserve the existing project-group status resolution behavior for all
other statuses.

In `@apps/web/src/session-logic.ts`:
- Around line 674-710: Update isAgentInternalActivity so agentId ownership alone
does not classify nested agent task rows as internal; use
isBackgroundTaskActivity() to distinguish shell-only background tasks from agent
task activity. Preserve nested agent rows and their CTA anchors while keeping
work-log, Agentic Activities, and spawn CTA classification consistent with the
shared task-type contract.

In `@packages/client-runtime/src/state/subagentRuntime.ts`:
- Around line 469-473: Update the status resolution in the flow using
TASK_COMPLETED_STATUS to validate that the looked-up value is a valid
RuntimeSubagentStatus before passing it to applyStatus. Guard against inherited
keys such as “toString” and “constructor” so invalid payload statuses fall back
to “completed,” rather than relying on truthiness or nullish coalescing alone.

---

Nitpick comments:
In `@apps/server/src/orchestration/ActivityPayloadProjection.test.ts`:
- Around line 5-15: The activity helper currently hardcodes the tool kind,
preventing task-lifecycle tests from exercising the task branch. Update activity
and its callers so each test supplies the appropriate activity kind, preserving
tool.completed for tool tests and using the task lifecycle kind in the second
test.

In `@apps/server/src/orchestration/workflowScriptQuery.test.ts`:
- Around line 10-15: The test currently writes fixtures into the real home
directory and uses a colliding fixed temporary filename. Make the scripts root
injectable in the relevant workflow script query API, defaulting to
scriptsRoot(), then have the test create an isolated mkdtempSync directory and
place both fixtures under it. Update calls and cleanup to use only this
temporary directory, removing afterAll cleanup of real user state.

In `@apps/server/src/orchestration/workflowScriptQuery.ts`:
- Around line 25-27: Document the provider decision in the module header for the
hardcoded scriptsRoot() path: explicitly state the intended workflow-script
behavior for Codex, Claude, Cursor, Grok, and OpenCode, including that only
Claude currently persists scripts if that is the supported behavior. Ensure the
documentation makes clear which providers support the Agents “script” affordance
and avoids implying that all adapters use ~/.claude/projects.

In `@apps/server/src/provider/Layers/ClaudeAdapter.test.ts`:
- Around line 1453-1601: Add focused harness-style tests in
ClaudeAdapter.test.ts for task_updated and workflow member synthesis. Cover
killed→cancelled and paused→idle status mapping, terminal patch removal from
liveTaskIds, endedAt conversion, and emitWorkflowMemberProgress behavior
including <coordinatorTaskId>:wf:<index> IDs, per-index deduplication, and
workflowAgentStatus.

In `@apps/server/src/provider/Layers/ClaudeAdapter.ts`:
- Around line 4358-4375: Update the stop-task cleanup around
context.query.stopTask to preserve best-effort per-task execution while logging
each rejection or thrown failure with the taskId and error details. Replace the
silent catch/ignore handling without allowing one failed stopTask call to
prevent remaining tasks from being attempted or the subsequent interrupt from
running.

In `@apps/server/src/provider/Layers/CodexAdapter.ts`:
- Around line 760-766: Document in a nearby comment to mapToRuntimeEvents that
Cursor, Grok, and OpenCode intentionally do not map collabAgent/... child task
events because those providers do not emit them; clarify that task.* child
events are currently supported only for Codex and Claude.

In `@apps/web/src/components/chat/MessagesTimeline.logic.ts`:
- Around line 531-533: The onlyToolEntries calculation currently uses
visibleGroupedEntries, which includes pinned spawn entries; update the logic
around onlyToolEntries to evaluate the hidden entries collection instead, while
preserving the existing workLogEntryIsToolLike predicate and toggle behavior.

In `@apps/web/src/rightPanelStore.ts`:
- Line 52: Revert RIGHT_PANEL_STORAGE_VERSION to 7 because
migratePersistedRightPanelState already preserves the newly added surface kind
and no reset is required.

In `@apps/web/src/session-logic.ts`:
- Around line 652-672: Move the quiet-timeline guarantee doc block from above
isAgentTaskStartedActivity to immediately above deriveWorkLogEntries, preserving
its text unchanged; keep the separate isAgentTaskStartedActivity documentation
directly above that function.
- Around line 882-895: Update DerivedWorkLogEntry and toDerivedWorkLogEntry to
preserve parentAgentId, then update agentSpawnGroupKey to use the copied
parentAgentId for workflow grouping when no legacy :wf: taskId encoding exists.
Keep the existing :wf: parsing precedence unchanged, and apply the parent-link
fallback before the coordinator and direct turnId cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1ee12d61-2e8a-40e0-b88a-6979acf94b14

📥 Commits

Reviewing files that changed from the base of the PR and between 64bf016 and 5ff1638.

📒 Files selected for processing (39)
  • apps/mobile/src/lib/threadActivity.ts
  • apps/server/integration/OrchestrationEngineHarness.integration.ts
  • apps/server/src/auth/RpcAuthorization.ts
  • apps/server/src/orchestration/ActivityPayloadProjection.test.ts
  • apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts
  • apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts
  • apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts
  • apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
  • apps/server/src/orchestration/ThreadBackgroundLiveness.test.ts
  • apps/server/src/orchestration/ThreadBackgroundLiveness.ts
  • apps/server/src/orchestration/runtimeLayer.ts
  • apps/server/src/orchestration/workflowScriptQuery.test.ts
  • apps/server/src/orchestration/workflowScriptQuery.ts
  • apps/server/src/provider/Layers/ClaudeAdapter.test.ts
  • apps/server/src/provider/Layers/ClaudeAdapter.ts
  • apps/server/src/provider/Layers/CodexAdapter.ts
  • apps/server/src/provider/Layers/CodexSessionRuntime.ts
  • apps/server/src/ws.ts
  • apps/web/src/components/AgentsPanel.tsx
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/RightPanelTabs.tsx
  • apps/web/src/components/Sidebar.logic.ts
  • apps/web/src/components/SidebarV2.tsx
  • apps/web/src/components/chat/MessagesTimeline.logic.ts
  • apps/web/src/components/chat/MessagesTimeline.tsx
  • apps/web/src/rightPanelStore.ts
  • apps/web/src/session-logic.test.ts
  • apps/web/src/session-logic.ts
  • packages/client-runtime/package.json
  • packages/client-runtime/src/state/orchestration.ts
  • packages/client-runtime/src/state/subagentRuntime.test.ts
  • packages/client-runtime/src/state/subagentRuntime.ts
  • packages/contracts/src/orchestration.ts
  • packages/contracts/src/providerRuntime.ts
  • packages/contracts/src/rpc.ts

Comment on lines +16 to +21
const link = NodePath.join(root, "sneaky.js");
try {
NodeFS.symlinkSync(outside, link);
} catch {
// pre-existing from a prior run
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The symlink escape test can pass without testing containment.

The catch block at Lines 19-21 swallows every symlinkSync failure, including EPERM on Windows and any unexpected error. If the link does not exist, readWorkflowScript fails with reason: "not-found" and assert.equal(sneaky._tag, "Failure") still passes. The assertion then proves nothing about realpath re-containment.

Verify the link exists, and assert the failure reason instead of only _tag.

💚 Proposed fix to assert the containment reason
 const link = NodePath.join(root, "sneaky.js");
-try {
-  NodeFS.symlinkSync(outside, link);
-} catch {
-  // pre-existing from a prior run
-}
+NodeFS.rmSync(link, { force: true });
+NodeFS.symlinkSync(outside, link);
       const escaped = yield* Effect.exit(readWorkflowScript({ scriptPath: outside }));
-      assert.equal(escaped._tag, "Failure");
+      assert.equal(escaped._tag, "Failure");
+      assert.equal(Cause.squash(escaped.cause).reason, "outside-root");
       // A symlink INSIDE the root pointing outside must also fail (realpath
       // re-containment of the leaf).
       const sneaky = yield* Effect.exit(readWorkflowScript({ scriptPath: link }));
-      assert.equal(sneaky._tag, "Failure");
+      assert.equal(sneaky._tag, "Failure");
+      assert.equal(Cause.squash(sneaky.cause).reason, "outside-root");

Also applies to: 48-57

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/orchestration/workflowScriptQuery.test.ts` around lines 16 -
21, Update the symlink setup and assertions in the workflow script query test
around NodeFS.symlinkSync and readWorkflowScript: only tolerate an
already-existing link, rethrow unexpected or permission errors, verify the link
exists before querying, and assert that the returned Failure reason is the
containment-specific value rather than checking only _tag.

Comment on lines +29 to +38
export const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")(function* (input: {
readonly scriptPath: string;
}) {
const requested = input.scriptPath;

if (!NodePath.isAbsolute(requested) || NodePath.extname(requested) !== ".js") {
return yield* Effect.fail(
new OrchestrationGetWorkflowScriptError({ reason: "invalid-path", scriptPath: requested }),
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find recorded workflow script paths in server state/contracts.
rg -n --type=ts -C4 'runHandles|scriptPath' apps/server/src packages/contracts/src | head -200

Repository: pingdotgg/t3code

Length of output: 17052


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## workflowScriptQuery.ts"
sed -n '1,130p' apps/server/src/orchestration/workflowScriptQuery.ts | cat -n

echo
echo "## ws.ts around readWorkflowScript call"
rg -n --type=ts -C8 'readWorkflowScript|OrchestrationGetWorkflowScriptInput|workflowScript|threadId' apps/server/src/ws.ts

echo
echo "## orchestration state/services that persist runHandles or thread workflows"
rg -n --type=ts -C3 'runHandles|scriptPath|Thread|Orchestration.*Workflow|workflow.*Path|projectDir|project' apps/server/src/orchestration apps/server/src | head -300

Repository: pingdotgg/t3code

Length of output: 49100


Scope workflow script reads by thread before trusting the path hint.

readWorkflowScript(input: { scriptPath }) checks only the absolute .js path against ~/.claude/projects, so it does not enforce the contract comment that the path is a hint from runHandles.scriptPath. getWorkflowScript also passes only input.scriptPath, losing threadId. Add server-side scoping for the caller’s thread and compare the resolved script path against the recorded workflow script location; use ~/.claude/projects only as a secondary bound.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/orchestration/workflowScriptQuery.ts` around lines 29 - 38,
Update readWorkflowScript and getWorkflowScript to carry the caller’s threadId,
then resolve the workflow script location recorded for that thread in
runHandles.scriptPath before accepting the requested path hint. Require the
resolved requested path to match the thread’s recorded script location, while
retaining ~/.claude/projects as a secondary containment check. Reject mismatches
through the existing invalid-path error flow.

Comment on lines +669 to +700
case "collabAgent/tokenUsage": {
// Cumulative per child thread: always the `total` breakdown, never
// `last` (which shrinks on follow-ups). Client folds max-merge.
const tokenUsage =
typeof payload.tokenUsage === "object" && payload.tokenUsage !== null
? (payload.tokenUsage as Record<string, unknown>)
: undefined;
const total =
typeof tokenUsage?.total === "object" && tokenUsage.total !== null
? (tokenUsage.total as Record<string, unknown>)
: undefined;
const totalTokens = typeof total?.totalTokens === "number" ? total.totalTokens : undefined;
if (totalTokens === undefined) {
return [];
}
const count = (value: unknown): number | undefined =>
typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
const typedUsage: RuntimeTaskUsage = {
totalTokens,
...(count(total?.inputTokens) !== undefined
? { inputTokens: count(total?.inputTokens) }
: {}),
...(count(total?.cachedInputTokens) !== undefined
? { cachedInputTokens: count(total?.cachedInputTokens) }
: {}),
...(count(total?.outputTokens) !== undefined
? { outputTokens: count(total?.outputTokens) }
: {}),
...(count(total?.reasoningOutputTokens) !== undefined
? { reasoningOutputTokens: count(total?.reasoningOutputTokens) }
: {}),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Validate totalTokens with the same rule as the other counts.

Line 680 accepts any number, including NaN, Infinity, and negative values, while every other field passes through count. RuntimeTaskUsage.totalTokens is NonNegativeInt, so a malformed wire value produces a payload that violates the contract. Reuse count for totalTokens as well. The same change removes the duplicated count(...) calls per field.

🐛 Proposed fix
-      const totalTokens = typeof total?.totalTokens === "number" ? total.totalTokens : undefined;
-      if (totalTokens === undefined) {
-        return [];
-      }
       const count = (value: unknown): number | undefined =>
         typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
+      const totalTokens = count(total?.totalTokens);
+      if (totalTokens === undefined) {
+        return [];
+      }
+      const inputTokens = count(total?.inputTokens);
+      const cachedInputTokens = count(total?.cachedInputTokens);
+      const outputTokens = count(total?.outputTokens);
+      const reasoningOutputTokens = count(total?.reasoningOutputTokens);
       const typedUsage: RuntimeTaskUsage = {
         totalTokens,
-        ...(count(total?.inputTokens) !== undefined
-          ? { inputTokens: count(total?.inputTokens) }
-          : {}),
-        ...(count(total?.cachedInputTokens) !== undefined
-          ? { cachedInputTokens: count(total?.cachedInputTokens) }
-          : {}),
-        ...(count(total?.outputTokens) !== undefined
-          ? { outputTokens: count(total?.outputTokens) }
-          : {}),
-        ...(count(total?.reasoningOutputTokens) !== undefined
-          ? { reasoningOutputTokens: count(total?.reasoningOutputTokens) }
-          : {}),
+        ...(inputTokens !== undefined ? { inputTokens } : {}),
+        ...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
+        ...(outputTokens !== undefined ? { outputTokens } : {}),
+        ...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
       };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case "collabAgent/tokenUsage": {
// Cumulative per child thread: always the `total` breakdown, never
// `last` (which shrinks on follow-ups). Client folds max-merge.
const tokenUsage =
typeof payload.tokenUsage === "object" && payload.tokenUsage !== null
? (payload.tokenUsage as Record<string, unknown>)
: undefined;
const total =
typeof tokenUsage?.total === "object" && tokenUsage.total !== null
? (tokenUsage.total as Record<string, unknown>)
: undefined;
const totalTokens = typeof total?.totalTokens === "number" ? total.totalTokens : undefined;
if (totalTokens === undefined) {
return [];
}
const count = (value: unknown): number | undefined =>
typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
const typedUsage: RuntimeTaskUsage = {
totalTokens,
...(count(total?.inputTokens) !== undefined
? { inputTokens: count(total?.inputTokens) }
: {}),
...(count(total?.cachedInputTokens) !== undefined
? { cachedInputTokens: count(total?.cachedInputTokens) }
: {}),
...(count(total?.outputTokens) !== undefined
? { outputTokens: count(total?.outputTokens) }
: {}),
...(count(total?.reasoningOutputTokens) !== undefined
? { reasoningOutputTokens: count(total?.reasoningOutputTokens) }
: {}),
};
case "collabAgent/tokenUsage": {
// Cumulative per child thread: always the `total` breakdown, never
// `last` (which shrinks on follow-ups). Client folds max-merge.
const tokenUsage =
typeof payload.tokenUsage === "object" && payload.tokenUsage !== null
? (payload.tokenUsage as Record<string, unknown>)
: undefined;
const total =
typeof tokenUsage?.total === "object" && tokenUsage.total !== null
? (tokenUsage.total as Record<string, unknown>)
: undefined;
const count = (value: unknown): number | undefined =>
typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
const totalTokens = count(total?.totalTokens);
if (totalTokens === undefined) {
return [];
}
const inputTokens = count(total?.inputTokens);
const cachedInputTokens = count(total?.cachedInputTokens);
const outputTokens = count(total?.outputTokens);
const reasoningOutputTokens = count(total?.reasoningOutputTokens);
const typedUsage: RuntimeTaskUsage = {
totalTokens,
...(inputTokens !== undefined ? { inputTokens } : {}),
...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
...(outputTokens !== undefined ? { outputTokens } : {}),
...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/Layers/CodexAdapter.ts` around lines 669 - 700,
Update the token usage parsing in the collabAgent/tokenUsage case to define and
reuse the existing count validator before validating totalTokens. Require
totalTokens to pass count, return [] when it is undefined, and assign the
validated value to RuntimeTaskUsage; reuse local validated values for the other
fields instead of calling count repeatedly.

Comment on lines +1109 to +1122
case "thread/closed":
yield* emitEvent({
kind: "notification",
threadId: options.threadId,
...(child.spawnTurnId ? { turnId: child.spawnTurnId } : {}),
method: "collabAgent/closed",
payload: childIdentity,
});
return true;
default:
// Remaining child chatter (name updates, deltas, plan updates)
// stays out of the parent timeline and has no agent mapping yet.
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether `error` is a per-thread server notification that can target a child thread.
set -euo pipefail

rg -nP --type=ts -C4 'SERVER_NOTIFICATION_METHODS' apps/server/src/provider | head -n 60
rg -nP --type=ts -C3 '"error"' apps/server/src/provider/Layers/CodexSessionRuntime.ts

Repository: pingdotgg/t3code

Length of output: 2829


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and inspect the child notification handling plus Codex server notification registration.
ast-grep outline apps/server/src/provider/Layers/CodexSessionRuntime.ts --match registerServerNotification --view expanded || true
sed -n '1050,1140p' apps/server/src/provider/Layers/CodexSessionRuntime.ts
sed -n '1150,1445p' apps/server/src/provider/Layers/CodexSessionRuntime.ts
sed -n '70,530p' apps/server/src/provider/Layers/CodexSessionRuntime.ts

# Locate and inspect the Codex RPC protocol definitions around server notifications.
rg -n --type=ts 'SERVER_NOTIFICATION_METHODS|ServerNotificationMethod|type "error"|notification method|method: "error"' apps/server/src/providers apps/server/src/provider -C 4 | head -n 220

Repository: pingdotgg/t3code

Length of output: 34193


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the child routing, raw notification handling, and notification emission/Agent mapping paths.
sed -n '600,1020p' apps/server/src/provider/Layers/CodexSessionRuntime.ts
sed -n '1020,1135p' apps/server/src/provider/Layers/CodexSessionRuntime.ts
sed -n '1135,1250p' apps/server/src/provider/Layers/CodexSessionRuntime.ts

# Locate generated protocol and tests around Codex error notifications.
rg -n --type=ts 'error.*notification|SERVER_NOTIFICATION_METHODS|ServerNotificationMethod|thread/status/changed|systemError' -C 3 | head -n 260

# Inspect CodexAdapter.test.ts around the existing raw error notification handling.
sed -n '710,770p' apps/server/src/provider/Layers/CodexAdapter.test.ts

Repository: pingdotgg/t3code

Length of output: 25813


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect how provider events are transformed into task runtime events and error status.
rg -n --type=ts 'task.*event|ProviderTaskEvent|task.*(started|failed|complete|status|error)|notification.*collabAgent/statusChanged|collabAgent/statusChanged' -C 4 | sed -n '1,260p'

# Locate the task event reducer/event handling in adapters.
rg -n --type=ts 'case "notification"|notification.*kind|notification.kind|failed.*notification|systemError|statusChanged' apps/server/src/provider -C 4 | sed -n '1,260p'

# Inspect surrounding lines where no result would imply failure to locate the handling.
rg -n --type=ts 'case "notification"|notification\.kind|notification\\.kind|notification.kind' apps/server/src/provider -C 4 || true

Repository: pingdotgg/t3code

Length of output: 619


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact line counts and file contents without relying on line numbers from the previous sed output.
wc -l apps/server/src/provider/Layers/CodexSessionRuntime.ts
sed -n '1080,1128p' apps/server/src/provider/Layers/CodexSessionRuntime.ts
sed -n '690,705p' apps/server/src/provider/Layers/CodexSessionRuntime.ts

# Check generated protocol definitions for `error` notification shape.
rg -n 'method: "error"|type: "error"|params: .*\{\s*threadId|threadId.*params|notification.*error|ServerNotification' -C 3 $(git ls-files | rg '(effect-codex|CodexRpc|types|generated|protocol).*\.ts$') | sed -n '1,260p'

# Locate all statusChanged/task transition handling.
rg -n --type=ts 'statusChanged|systemError|status: "error"|task.*(fail|failed|complete|started|update|runtime)' apps/server/src/provider apps/server/src -C 3 | sed -n '1,320p'

Repository: pingdotgg/t3code

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect CodexProvider/CodexAdapter task-event conversion around statusChanged and error mapping.
sed -n '330,440p' apps/server/src/provider/Layers/CodexProvider.ts
sed -n '500,610p' apps/server/src/provider/Layers/CodexProvider.ts
sed -n '330,690p' apps/server/src/provider/Layers/CodexAdapter.ts

# Inspect generated error notification definition.
sed -n '1797,1895p' packages/effect-codex-app-server/src/_generated/schema.gen.ts
sed -n '409,425p' packages/effect-codex-app-server/src/_generated/meta.gen.ts

# Inspect existing tests around codex raw error notification and collab-agent status events if present.
rg -n --type=ts 'collabAgent/statusChanged|collabAgent.*statusChanged|systemError|shouldSuppressChildConversationNotification|interceptCollabChildNotification|error.*notification|NotificationMethod.*error' apps/server/src/provider/Layers/CodexAdapter.test.ts apps/server/src/provider/Layers/CodexProvider.test.ts -C 3 | sed -n '1,240p'

Repository: pingdotgg/t3code

Length of output: 24093


Map child error notifications to a Codex failure signal.

interceptCollabChildNotification returns true for every unrecognized child notification, so a child error notification is dropped before mapCollabAgentEvent. This leaves the child in its prior running state because only collabAgent/statusChanged with type: "systemError" maps to task.updated / status: "failed".

Handle case "error" and emit a failed collabAgent/statusChanged, or at least log the dropped error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/Layers/CodexSessionRuntime.ts` around lines 1109 -
1122, The interceptCollabChildNotification switch currently drops child error
notifications, leaving the child marked running. Add a case for "error" that
emits a collabAgent/statusChanged event with type "systemError" and the child
identity/status payload required by mapCollabAgentEvent to produce a failed task
update; preserve existing handling for closed and other notifications.

Comment on lines +504 to 512
const pinnedSpawnEntries = visibleGroupedEntries.filter(
(entry) => entry.agentSpawn !== undefined,
);
const hiddenEntries = overflowCandidates.slice(0, -MAX_VISIBLE_WORK_LOG_ENTRIES);
const visibleEntries = [
...pinnedSpawnEntries,
...overflowCandidates.slice(-MAX_VISIBLE_WORK_LOG_ENTRIES),
];
const renderedEntries = expanded ? [...hiddenEntries, ...visibleEntries] : visibleEntries;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Pinned spawn rows break the chronological order of the work group.

visibleEntries places every spawn entry before the retained tail entries, and renderedEntries places hiddenEntries before visibleEntries. A spawn row created in the middle of the group therefore renders above earlier tool rows, and expanding the group moves it again. Rows in this list are otherwise ordered by time.

Preserve group order and select the retained entries by index instead of concatenating two filtered lists.

♻️ Proposed order-preserving selection
-          const overflowCandidates = visibleGroupedEntries.filter(
-            (entry) => entry.agentSpawn === undefined,
-          );
-          const pinnedSpawnEntries = visibleGroupedEntries.filter(
-            (entry) => entry.agentSpawn !== undefined,
-          );
-          const hiddenEntries = overflowCandidates.slice(0, -MAX_VISIBLE_WORK_LOG_ENTRIES);
-          const visibleEntries = [
-            ...pinnedSpawnEntries,
-            ...overflowCandidates.slice(-MAX_VISIBLE_WORK_LOG_ENTRIES),
-          ];
+          const overflowCandidates = visibleGroupedEntries.filter(
+            (entry) => entry.agentSpawn === undefined,
+          );
+          const hiddenIds = new Set(
+            overflowCandidates.slice(0, -MAX_VISIBLE_WORK_LOG_ENTRIES).map((entry) => entry.id),
+          );
+          const hiddenEntries = visibleGroupedEntries.filter((entry) => hiddenIds.has(entry.id));
+          const visibleEntries = visibleGroupedEntries.filter((entry) => !hiddenIds.has(entry.id));

With this shape, renderedEntries for the expanded case becomes visibleGroupedEntries:

-          const renderedEntries = expanded ? [...hiddenEntries, ...visibleEntries] : visibleEntries;
+          const renderedEntries = expanded ? visibleGroupedEntries : visibleEntries;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const pinnedSpawnEntries = visibleGroupedEntries.filter(
(entry) => entry.agentSpawn !== undefined,
);
const hiddenEntries = overflowCandidates.slice(0, -MAX_VISIBLE_WORK_LOG_ENTRIES);
const visibleEntries = [
...pinnedSpawnEntries,
...overflowCandidates.slice(-MAX_VISIBLE_WORK_LOG_ENTRIES),
];
const renderedEntries = expanded ? [...hiddenEntries, ...visibleEntries] : visibleEntries;
const overflowCandidates = visibleGroupedEntries.filter(
(entry) => entry.agentSpawn === undefined,
);
const hiddenIds = new Set(
overflowCandidates.slice(0, -MAX_VISIBLE_WORK_LOG_ENTRIES).map((entry) => entry.id),
);
const hiddenEntries = visibleGroupedEntries.filter((entry) => hiddenIds.has(entry.id));
const visibleEntries = visibleGroupedEntries.filter((entry) => !hiddenIds.has(entry.id));
const renderedEntries = expanded ? visibleGroupedEntries : visibleEntries;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/chat/MessagesTimeline.logic.ts` around lines 504 -
512, Update the work-group entry selection around pinnedSpawnEntries,
hiddenEntries, visibleEntries, and renderedEntries to preserve the original
visibleGroupedEntries chronological order. Select retained entries by their
indices rather than concatenating all spawn rows with the overflow tail, and
ensure the expanded path renders the full ordered group while the collapsed path
renders only the retained ordered entries.

Comment on lines +4209 to +4224
const handleStopBackgroundWork = useCallback(async () => {
if (!activeThread) return;
setIsStoppingBackgroundWork(true);
const result = await interruptThreadTurn({
environmentId,
input: buildThreadTurnInterruptInput(activeThread),
});
if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
setIsStoppingBackgroundWork(false);
const error = squashAtomCommandFailure(result);
setThreadError(
activeThread.id,
error instanceof Error ? error.message : "Failed to stop background work.",
);
}
}, [activeThread, environmentId, interruptThreadTurn, setThreadError]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Clear isStoppingBackgroundWork when the interrupt command is interrupted.

The reset at line 4217 runs only when isAtomCommandInterrupted(result) is false. If the command itself is interrupted, no request reached the server, the liveness never clears, and the effect at line 4197 never fires. The Stop button then stays disabled and shows "Stopping..." for the rest of the thread session.

Reset the flag for every failure, and keep the error toast path unchanged.

🐛 Proposed fix
-    if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
-      setIsStoppingBackgroundWork(false);
-      const error = squashAtomCommandFailure(result);
-      setThreadError(
-        activeThread.id,
-        error instanceof Error ? error.message : "Failed to stop background work.",
-      );
-    }
+    if (result._tag === "Failure") {
+      setIsStoppingBackgroundWork(false);
+      if (!isAtomCommandInterrupted(result)) {
+        const error = squashAtomCommandFailure(result);
+        setThreadError(
+          activeThread.id,
+          error instanceof Error ? error.message : "Failed to stop background work.",
+        );
+      }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleStopBackgroundWork = useCallback(async () => {
if (!activeThread) return;
setIsStoppingBackgroundWork(true);
const result = await interruptThreadTurn({
environmentId,
input: buildThreadTurnInterruptInput(activeThread),
});
if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
setIsStoppingBackgroundWork(false);
const error = squashAtomCommandFailure(result);
setThreadError(
activeThread.id,
error instanceof Error ? error.message : "Failed to stop background work.",
);
}
}, [activeThread, environmentId, interruptThreadTurn, setThreadError]);
const handleStopBackgroundWork = useCallback(async () => {
if (!activeThread) return;
setIsStoppingBackgroundWork(true);
const result = await interruptThreadTurn({
environmentId,
input: buildThreadTurnInterruptInput(activeThread),
});
if (result._tag === "Failure") {
setIsStoppingBackgroundWork(false);
if (!isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
setThreadError(
activeThread.id,
error instanceof Error ? error.message : "Failed to stop background work.",
);
}
}
}, [activeThread, environmentId, interruptThreadTurn, setThreadError]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/ChatView.tsx` around lines 4209 - 4224, Update
handleStopBackgroundWork so every interruptThreadTurn failure resets
isStoppingBackgroundWork, including when isAtomCommandInterrupted(result) is
true, while preserving the existing error-setting path only for non-interrupted
failures.

"Awaiting Input": 4,
Working: 3,
Connecting: 3,
Monitoring: 3,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Print the full THREAD_STATUS_PRIORITY map.
rg -nP -A14 'THREAD_STATUS_PRIORITY' apps/web/src/components/Sidebar.logic.ts

Repository: pingdotgg/t3code

Length of output: 1026


🏁 Script executed:

#!/bin/bash
set -e
echo "== Sidebar.logic.ts outline around resolveProjectStatusIndicator && statuses =="
rg -n "function resolveProjectStatusIndicator|resolveProjectStatusIndicator\\(|ThreadStatusPill|Working|Connecting|Monitoring" apps/web/src/components/Sidebar.logic.ts

echo
echo "== Relevant function context =="
sed -n '120,150p;666,692p' apps/web/src/components/Sidebar.logic.ts

Repository: pingdotgg/t3code

Length of output: 2522


Keep Monitoring below active work priorities.

Working, Connecting, and Monitoring all use priority 3, and resolveProjectStatusIndicator only advances on strict >. Sorting by priorities can therefore return the first Monitoring entry instead of an active Working/Connecting entry for the project group. Assign Monitoring a lower priority, or change the tie-break to prefer active work statuses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/Sidebar.logic.ts` at line 135, Update the status
priority mapping around Monitoring so Monitoring ranks below the active Working
and Connecting statuses, or adjust resolveProjectStatusIndicator to break
equal-priority ties in favor of active work. Preserve the existing project-group
status resolution behavior for all other statuses.

Comment on lines +443 to +450
// Background work outlives the turn: fleets read as working; monitoring
// only when watch loops are the sole live work.
if (thread.backgroundLiveness === "working") {
return "working";
}
if (thread.backgroundLiveness === "monitoring") {
return "monitoring";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Background-liveness checks were inserted above higher-signal states in both status resolvers. Both functions now return a background status before states that a user must act on or that report failure, so lingering background work masks them.

  • apps/web/src/components/Sidebar.logic.ts#L443-L450: move the two backgroundLiveness branches below the session?.status === "error" check, or confirm that the server clears backgroundLiveness on session error.
  • apps/web/src/components/Sidebar.logic.ts#L626-L646: move the two backgroundLiveness pill branches below the hasPlanReadyPrompt check so an actionable plan prompt still wins.
📍 Affects 1 file
  • apps/web/src/components/Sidebar.logic.ts#L443-L450 (this comment)
  • apps/web/src/components/Sidebar.logic.ts#L626-L646
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/Sidebar.logic.ts` around lines 443 - 450, Reorder the
backgroundLiveness branches in both status resolvers in
apps/web/src/components/Sidebar.logic.ts:443-450 and
apps/web/src/components/Sidebar.logic.ts:626-646. In the first resolver, keep
session?.status === "error" ahead of background statuses; in the second, keep
hasPlanReadyPrompt ahead of the background-liveness pill branches so error and
actionable states take precedence.

Comment on lines +674 to +710
function isAgentInternalActivity(activity: OrchestrationThreadActivity): boolean {
const payload =
activity.payload && typeof activity.payload === "object"
? (activity.payload as Record<string, unknown>)
: null;
if (!payload) {
return false;
}
// A task owned by an agent (a subagent's own background shell) is
// agent-internal regardless of bypass tagging.
if (
(activity.kind === "task.started" ||
activity.kind === "task.progress" ||
activity.kind === "task.updated" ||
activity.kind === "task.completed") &&
typeof payload.agentId === "string" &&
payload.agentId.trim().length > 0
) {
return true;
}
if (payload.timelineBypass === true) {
// Bypassed agent lifecycle rows still feed the spawn CTA: collapse folds
// every such row into its batch's single row, so letting them through
// preserves the quiet-timeline invariant while giving Codex children —
// whose rows are ALL bypassed — a CTA anchor (wire-probe finding: no
// CTA ever formed for a Codex fleet). task.started included so the CTA
// anchors at the spawn point, not the first progress tick.
const isAgentTaskRow =
(activity.kind === "task.started" ||
activity.kind === "task.progress" ||
activity.kind === "task.completed") &&
typeof payload.taskId === "string" &&
!isBackgroundTaskActivity(payload);
return !isAgentTaskRow;
}
return typeof payload.agentId === "string" && payload.agentId.trim().length > 0;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find provider emissions that set both agentId and taskType on task payloads.
rg -nP -C6 '\bagentId\s*[:=]' apps/server/src/provider --glob '*.ts' | rg -nP -C6 'taskType'

Repository: pingdotgg/t3code

Length of output: 2658


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -a 'session-logic\.ts|subagentRuntime\.ts' . | sed 's#^\./##'

echo
echo "== session-logic outline/section =="
for f in $(fd 'session-logic\.ts' .); do
  echo "--- $f ---"
  wc -l "$f"
  sed -n '630,735p' "$f" | cat -n
done

echo
echo "== subagentRuntime relevant section =="
for f in $(fd 'subagentRuntime\.ts' .); do
  echo "--- $f ---"
  wc -l "$f"
  sed -n '130,190p' "$f" | cat -n
done

echo
echo "== all definitions/usages of isBackgroundTaskActivity =="
rg -n "isBackgroundTaskActivity|backgroundTask|taskType|agentId" -g '*.ts' .

Repository: pingdotgg/t3code

Length of output: 28534


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import re
from pathlib import Path

subagent = Path("packages/client-runtime/src/state/subagentRuntime.ts").read_text()
session = Path("apps/web/src/session-logic.ts").read_text()

# Extract definitions for behavioral inspection as data.
is_non_agent_match = re.search(r'function isNonAgentTaskType\(taskType: string \| undefined\): boolean {\s*(.*?)\n}', subagent, re.S)
is_bg_match = re.search(r'export function isBackgroundTaskActivity\(payload: Record<string, unknown>\): boolean \{([\s\S]*?)(?=\n})\n\}', subagent)
bg_branch = re.search(r'if \(ownedByAgent\) \{([\s\S]*?)\n\}', is_bg_match.group(1), re.S)
internal_branch = re.search(r'if \(\s*\(([\s\S]*?)&& payload\.agentId\.trim\(\)\.length > 0\s*\)\s*\)\s*\{\s*return true;\n\s*\}', session, re.S)

print("isNonAgentTaskType taskTypes:")
if is_non_agent_match:
    types = re.findall(r'["\']([^"'\s]+)["\']', is_non_agent_match.group(1))
    print(types)

owned_bg_branch = bg_branch.group(1).strip() if bg_branch else "<missing>"
print("\nownByAgent branch body:")
print(owned_bg_branch)

print("\ninternal branch contains isBackgroundTaskActivity:", "isBackgroundTaskActivity(payload)" in internal_branch.group(1) if internal_branch else False)
print("\ninternal agentId raw condition:", bool(internal_branch))

# Static invariant checks over literal source strings.
ownedByAgent_has_is_non_agent_task_type = "isNonAgentTaskType(taskType)" in owned_bg_branch and "taskType === undefined" in owned_bg_branch
agentBranch_excludes_is_background = bool(internal_branch) and "isBackgroundTaskActivity(payload)" not in internal_branch.group(1)
agentBranch_contains_agentId_condition = bool(internal_branch) and re.search(r'typeof\s+payload\.agentId\s*===\s*["\']string["\']', internal_branch.group(1))
agentBranch_contains_nonempty_agentId_condition = bool(internal_branch) and re.search(r'payload\.agentId\.trim\(\)\.length\s*>\s*0', internal_branch.group(1))
print("\nownedByAgent branch uses taskType absent-or-non-agent classifier:", ownedByAgent_has_is_non_agent_task_type)
PY

Repository: pingdotgg/t3code

Length of output: 363


Keep nested agent task rows in the activity/CTA classification.

payload.agentId marks ownership, not shell-only behavior here. Runtime classifier keeps rows with an agent task type, so hiding all task.* rows that only carry agentId removes nested agents and their anchors. Reuse isBackgroundTaskActivity() in this branch so work-log, Agentic Activities, and spawn CTA use the same task type contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/session-logic.ts` around lines 674 - 710, Update
isAgentInternalActivity so agentId ownership alone does not classify nested
agent task rows as internal; use isBackgroundTaskActivity() to distinguish
shell-only background tasks from agent task activity. Preserve nested agent rows
and their CTA anchors while keeping work-log, Agentic Activities, and spawn CTA
classification consistent with the shared task-type contract.

Comment on lines +469 to +473
const TASK_COMPLETED_STATUS: Record<string, RuntimeSubagentStatus> = {
completed: "completed",
failed: "failed",
stopped: "interrupted",
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the TASK_COMPLETED_STATUS lookup against inherited keys.

TASK_COMPLETED_STATUS is a plain object literal, and line 632 indexes it with an unvalidated payload string. A payload with status: "toString" (or "constructor") resolves to an inherited Function value. That value is truthy, so ?? "completed" does not apply, and applyStatus stores a function as the agent status. STATUS_VISUALS[status] in apps/web/src/components/AgentsPanel.tsx (line 57 and line 143) then returns undefined and the panel throws on .dotClass. The module comment on line 431 confirms payloads are not schema-validated on the read path.

Validate the resolved value instead of trusting the lookup.

🛡️ Proposed fix using a null-prototype map and an explicit check
-const TASK_COMPLETED_STATUS: Record<string, RuntimeSubagentStatus> = {
-  completed: "completed",
-  failed: "failed",
-  stopped: "interrupted",
-};
+const TASK_COMPLETED_STATUS: ReadonlyMap<string, RuntimeSubagentStatus> = new Map([
+  ["completed", "completed"],
+  ["failed", "failed"],
+  ["stopped", "interrupted"],
+]);
-        const status = TASK_COMPLETED_STATUS[asString(payload.status) ?? ""] ?? "completed";
+        const status = TASK_COMPLETED_STATUS.get(asString(payload.status) ?? "") ?? "completed";

Also applies to: 632-632

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client-runtime/src/state/subagentRuntime.ts` around lines 469 - 473,
Update the status resolution in the flow using TASK_COMPLETED_STATUS to validate
that the looked-up value is a valid RuntimeSubagentStatus before passing it to
applyStatus. Guard against inherited keys such as “toString” and “constructor”
so invalid payload statuses fall back to “completed,” rather than relying on
truthiness or nullish coalescing alone.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5d68958. Configure here.

// progress row per task instead of thousands, so a large fleet's
// ticks can no longer evict its own start/terminal rows out of
// the 500-row retention window.
id: EventId.make(`task-progress:${event.payload.taskId}`),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-thread progress ID collision

High Severity

Stable progress activity ids are keyed only by taskId (task-progress:… / tool-progress:…), but projection_thread_activities.activity_id is a global primary key whose upsert also rewrites thread_id. Claude task ids are short session-local values, and this file already scopes the same identity via providerTaskKey(threadId, taskId). A collision moves or overwrites another thread’s progress row.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5d68958. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant